# 6函数
# https://liaoxuefeng.com/books/python/function/call-function/index.html
#====================================================================================================
#6.1 调用函数
# help（函数名）查看函数使用帮助
# 函数名其实就是指向一个函数对象的引用，完全可以把函数名赋给一个变量，相当于给这个函数起了一个“别名”：
a = abs
print(a(-1))
#练习题
# 请利用Python内置的hex()函数把一个整数转换成十六进制表示的字符串：
help(hex)
print (hex(255))
#====================================================================================================
#6.2 定义函数
# def 函数名（参数）：
#     函数体
#     return 返回值
# 函数体内部的语句在执行时，一旦执行到return时，函数就执行完毕，并将结果返回
# 如果没有return语句，函数执行完毕后也会返回结果，只是结果为None
# 跨文件引用自定义函数：需要在文件开头加上from 文件名 import 函数名
# 函数的参数:错误检查：数量或类型不对
#空语句pass:用来作为占位符，保持程序结构的完整性(在没有想好函数体的时候)
#返回多个值：返回值是一个tuple
#实际上返回值仍然是单一值，但是返回的是一个tuple，多个变量可以同时接收一个tuple，按位置赋给对应的值
import math
def move(x,y,step,angle=0):
    nx = x + step * math.cos(angle)
    ny = y + step * math.sin(angle)
    return nx,ny
x,y = move(100,100,60,math.pi/6)#这个元组可以直接赋值给x,y
print(x,y)
print(move(100,100,60,math.pi/6))
#练习题
# 请定义一个函数quadratic(a, b, c)，接收3个参数，返回一元二次方程：ax2 + bx + c = 0 的两个解。
def quadratic(a,b,c):
    if b**2-4*a*c<0:
        print('无解')
    else:
        x1=(-b+math.sqrt(b**2-4*a*c))/(2*a)
        x2=(-b-math.sqrt(b**2-4*a*c))/(2*a)
        print('x1=%.2f,x2=%.2f'%(x1,x2))
quadratic(1,2,1)
#====================================================================================================
#6.3 函数的参数
#默认参数：必选参数在前，默认参数在后，当函数有多个参数时，把变化大的参数放前面，变化小的参数放后面，变化小的参数就可以作为默认参数
#位置参数：一般、常用
#默认参数
def power(x,n=2):
    s=1
    while n>0:
        s*=x
        n=n-1
    return s
# 不传入n时默认n为2
#定义默认参数要牢记一点：默认参数必须指向不变对象！
#典型案例
def add_end(L=[]):
    L.append('END')
    return L
# 连续调用add_end函数
add_end()
add_end()
print(add_end())
#结果是['END', 'END']
#因为这里面L是个变量名称，每次调用这个函数时使用到默认参数时都会改变这个变量
#正确方法
def add_end(L=None):
    if(L==None):L=['END']
    else:L.append('END')
    return L
add_end()
add_end()
print(add_end())
#为什么要设计str、None这样的不变对象呢？因为不变对象一旦创建，对象内部的数据就不能修改，这样就减少了由于修改数据导致的错误。此外，由于对象不变，多任务环境下同时读取对象不需要加锁，同时读一点问题都没有。我们在编写程序时，如果可以设计一个不变对象，那就尽量设计成不变对象。
#可变参数
#例题：给定一组数字a，b，c……，请计算a2 + b2 + c2 + ……。
#法一：参数用list或者tuple形式
def func1(numbers):
    s=0
    for i in numbers:
         s+=i**2
    return s
#使用
list1=[1,2,3]
tuple=(1,2,3,4)
print(func1(list1))
print(func1(tuple))
#法二:可变参数*
def func2(*args):
    s=0
    for i in args:s+=i**2
    return s
#定义可变参数和定义一个list或tuple参数相比，仅仅在参数前面加了一个*号。在函数内部，参数numbers接收到的是一个tuple，因此，函数代码完全不变。但是，调用该函数时，可以传入任意个参数，包括0个参数：
print(func2(1,2,3))
tuple1=(1,2,3,4)
print(func2(*tuple))#*nums表示把nums这个list或者tuple的所有元素作为可变参数传进去。
#关键字参数
#而关键字参数允许你传入0个或任意个含参数名的参数，这些关键字参数在函数内部自动组装为一个dict
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)
person('aa',18,city='chengdu',grade='F')
#和可变参数类似，也可以先组装出一个dict，然后，把该dict转换为关键字参数传进去
dict1={'city':'luzhou','grade':'F'}
person('bb',19,**dict1)
#**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数，kw将获得一个dict，注意kw获得的dict是extra的一份拷贝，对kw的改动不会影响到函数外的extra
#命名关键字参数
#和关键字参数**kw不同，命名关键字参数需要一个特殊分隔符*，*后面的参数被视为命名关键字参数
#限制关键字的名字、只接收city,job
def person2(name,age,*,city,job):
    print(name,age,city,job)
#如果函数定义中已经有了一个可变参数，后面跟着的命名关键字参数就不再需要一个特殊分隔符*了：
def person(name, age, *args, city, job):
    print(name, age, args, city, job)
#命名关键字参数必须传入参数名
#参数组合
#在Python中定义函数，可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数，这5种参数都可以组合使用。但是请注意，参数定义的顺序必须是：必选参数、默认参数、可变参数、命名关键字参数和关键字参数。python解释器会按照默认的顺序填入
def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
#对于任意函数，都可以通过类似func(*args, **kw)的形式调用它，无论它的参数是如何定义的。
args=(1,2,3,4)
kw={'q':"A",'w':'B'}
f1(*args,**kw)
#结果：a = 1 b = 2 c = 3 args = (4,) kw = {q: 'A', w: 'B'}
#====================================================================================================
#6.4 递归函数
#递归函数：在函数内部调用自己
# 我们来计算阶乘n! = 1 x 2 x 3 x ... x n，用函数fact(n)表示
# fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n
def fact(n):
    if n==1:return 1
    else:return n*fact(n-1)
print(fact(5))
print(fact(100))
#堆栈溢出：递归调用的次数过多，会导致栈溢出。函数调用是通过栈（stack）这种数据结构实现的，每当进入一个函数调用，栈就会加一层栈帧，每当函数返回，栈就会减一层栈帧。由于栈的大小不是无限的，所以，递归调用的次数过多，会导致栈溢出。
# print(fact(1000))#RecursionError: maximum recursion depth exceeded in comparison,堆栈溢出
#尾递归优化：在函数返回的时候，调用自身，并且return语句不能包含表达式。上一个例子因为返回值包含了n*fact(n-1)所以每次返回都要占用一个栈帧，所以不是尾递归
# 上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式，所以就不是尾递归了。要改成尾递归方式，需要多一点代码，主要是要把每一步的乘积传入到递归函数中：
def fact(n):
    if n==1:return 1
    else:return fact_iter(n,1)
def fact_iter(num, product):
    if num==1:return product
    else:return fact_iter(num-1,num*product)
print(fact(5))
# print(fact(1000))#RecursionError: maximum recursion depth exceeded in comparison,堆栈溢出
# 尾递归调用时，如果做了优化，栈不会增长，因此，无论多少次调用也不会导致栈溢出。
# 遗憾的是，大多数编程语言没有针对尾递归做优化，Python解释器也没有做优化，所以，即使把上面的fact(n)函数改成尾递归方式，也会导致栈溢出。
#！！！！！！！！！！！所以即使是尾递归在python中也会溢出
#练习题：汉诺塔移动 请编写move(n, a, b, c)函数，它接收参数n，表示3个柱子A、B、C中第1个柱子A的盘子数量，然后打印出把所有盘子从A借助B移动到C的方法
#第一步：将上面n-1个移动到B   第二步：将最后一个移动到C   第三步将n-1个移动到A
#第四步：重复上面操作，三步为一个整体
step=0
def move(n,a,b,c):
    global step
    if n==1:
        step+=1
        print('第%d步：'%step,a,'-->',c)
    else:
        move(n-1,a,c,b)
        move(1,a,b,c)
        move(n-1,b,a,c)
move(3,'A','B','C')
print('总共%d步'%step)